/* $Date: 2009-09-07 16:12:19 -0300 (lun 07 de sep de 2009) $
 * $Revision: 990 $
 */

/* 
 *  kbleds.c - Hace parpadear los LEDs del teclado hasta que se descarga
 *  el módulo.
 *
 *  Código original http://tldp.org/LDP/lkmpg/2.6/html/x1194.html
 *  Autor: Daniele Paolo Scarpazza
 */

#define FALSE 0
#define TRUE (!FALSE)

typedef int Bool;

#include <linux/module.h>
#include <linux/version.h>		/* Para LINUX_VERSION_CODE, etc. */
#include <linux/tty.h>			/* Para MAX_NR_CONSOLES */
#include <linux/kd.h>			/* Para KDSETLED */
#include <linux/console_struct.h>	/* Para vc_cons */
#include <linux/sched.h>		/* Para nr_running() */ 
#include <linux/mm.h>
#include <linux/vmstat.h>

MODULE_DESCRIPTION("Ejemplo que ilustra el uso de los leds del teclado");
MODULE_AUTHOR("Catedra Sistemas Operativos FaMAF");
MODULE_LICENSE("GPL");

struct tty_struct *my_tty = NULL;
struct tty_driver *my_driver = NULL;
struct timer_list my_timer;
char kbledstatus = 0;


/* Definición condicional para poder compilar en kernels < 2.6.26 donde
 * tty_driver no tiene el campo ops y la función ioctl se accede directamente.
 */ 
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,26)
#define IOCTL(tty_drv) (tty_drv->ioctl)
#else
#define IOCTL(tty_drv) (tty_drv->ops->ioctl)
#endif


#define BLINK_DELAY		HZ/5
#define ALL_LEDS_ON   	0x07
#define RESTORE_LEDS  	0xFF
#define SCROLL_LOCK	  	0x01
#define NUM_LOCK		0x02
#define CAPS_LOCK		0x04

/* La función my_timer_func hace parpadear los LEDs del teclado periódicamente
 * llamando al comando KDSETLED del ioctl() sobre el controlador del teclado.
 * Para aprender más acerda de las operaciones ioctl sobre una terminal virtual
 * mirar el archivo:
 *     /usr/src/linux/drivers/char/vt_ioctl.c, function vt_ioctl().
 *
 * El argumento de KDSETLED se establece alternativamente en 7 haciendo que
 * el modo de los LEDs sea puesto en LED_SHOW_IOCTL, y todos las lucese se
 * encienden. El otro valor es 0xFF (255) que vuelve los LEDs al estado
 * LEDS_SHOW_FLAGS y las luces reflejan el estado actual del teclado.
 * Para leer más sobre este tema hay que mirar el archivo
 *     /usr/src/linux/drivers/char/keyboard.c, function setledstate().
 */

/* La función get_nr_bits cuenta la cantidad mínima de bits necesarios para
 * representar el número almacenado en la variable "var
 */

unsigned long * set_pstatus (unsigned long * pstatus, unsigned long led_value)
{
	Bool num_on = FALSE;
	Bool caps_on = FALSE;
	Bool scroll_on = FALSE;
	unsigned long aux = led_value;
	*pstatus = 0x00;

	aux = aux & SCROLL_LOCK;
	if (aux == SCROLL_LOCK) /*prender SCROLL_LOCK?*/
	{
		scroll_on = TRUE;
		*pstatus = *pstatus | SCROLL_LOCK; /*lo prendemos*/
	}
	aux = led_value;
	aux = aux & NUM_LOCK;
	if (aux == NUM_LOCK) /*prender NUM_LOCK?*/
	{
		caps_on = TRUE;
		*pstatus = *pstatus | CAPS_LOCK; /*porq estan cambiados*/
	}
	aux = led_value;
	aux = aux & CAPS_LOCK;
	if (aux == CAPS_LOCK) /*prender CAPS_LOCK?*/
	{
		num_on = TRUE;
		*pstatus = *pstatus | NUM_LOCK; /*porq estan cambiados */
	}

	return pstatus;
}	

unsigned long get_nr_bits (unsigned long var)
{
	unsigned long ret = 1;
	unsigned long aux = 1;
	Bool alcanza = FALSE;

	while (!alcanza)
	{
		if (var <= aux)
			alcanza = TRUE;
		else
			ret ++;
		
		aux *= 2;
	}

	ret -=1;
	while ( (ret%4) != 0)
		ret++;	

	return ret;
}

static void my_timer_func(unsigned long ptr)
{
	unsigned long *pstatus = (unsigned long *)ptr;
	unsigned long vm_status[NR_VM_EVENT_ITEMS];
	unsigned long nr_pgfaults = 0;
	unsigned long nr_var_bits = 0;
	unsigned long led_value = 0x00;

	BUG_ON(!pstatus);

	all_vm_events (vm_status);
	nr_pgfaults = vm_status[PGFAULT];
	nr_var_bits = get_nr_bits (nr_pgfaults);
	led_value = (nr_pgfaults >> (nr_var_bits - 3)) & 0x07;

	/*printk (KERN_INFO "nr_pgfaults = %lX\n", nr_pgfaults);
	printk (KERN_INFO "nr_bits = %ld\n", nr_var_bits);
	printk (KERN_INFO "led_value = %lX\n", led_value);*/

	if (*pstatus == RESTORE_LEDS)
		pstatus = set_pstatus (pstatus, led_value);
	else
		*pstatus = RESTORE_LEDS;

	IOCTL(my_driver)(my_tty, NULL, KDSETLED, *pstatus);

	my_timer.expires = jiffies + BLINK_DELAY;
	add_timer(&my_timer);
}

static int __init kbleds_init(void)
{
	printk(KERN_INFO "kbleds: loading\n");
	my_tty = get_current_tty();
	BUG_ON(!my_tty);

	printk (KERN_INFO "Counting number of page faults\n");

	my_driver = my_tty->driver;
	BUG_ON(!my_driver);

	/*
	 * Establece por primera vez el temporizador para el parpadeo.
	 */
	init_timer(&my_timer);
	my_timer.function = my_timer_func;
	my_timer.data = (unsigned long)&kbledstatus;
	my_timer.expires = jiffies + BLINK_DELAY;
	add_timer(&my_timer);

	return 0;
}

static void __exit kbleds_cleanup(void)
{
	printk(KERN_INFO "kbleds: unloading...\n");
	del_timer(&my_timer);
	IOCTL(my_driver)(my_tty, NULL, KDSETLED, RESTORE_LEDS);
}


module_init(kbleds_init);
module_exit(kbleds_cleanup);
